package main

// 请求到来，判断是否处于熔断期
//   若熔断期未过，返回预设熔断处理
//   读取当前请求次数及当前失败次数，当前请求次数+1
//     若请求超时或返回失败，则当前失败次数+1
//     判断是否达到请求阈值及失败率
//        若达到，读取上次熔断时间
//          若熔断期未过，运行预设熔断处理
//          若熔断期已过，更新熔断时间及重置请求次数失败次数

import (
	"errors"
	"log"
	"sync"
	"sync/atomic"
	"time"
)

type Breaker struct {
	BreakCount  int // 请求阈值，触发熔断首要满足条件
	FailPercent int // 失败率，触发熔断第二满足条件，可设置（1-99）
	BreakTime   int // 熔断时长，保持熔断，等待熔断时长后将尝试关闭熔断，单位秒

	nowCount      int   // 当前请求数
	failCount     int   // 当前请求失败数
	lastBreakTime int64 // 上次熔断时间
}

type Breakers map[string]*Breaker

var bs = make(Breakers)

func BreakHandle(breakName string, run func() (string, error), fallback func() (string, error)) (string, error) {
	// 判断是否处于熔断期
	now := time.Now().Unix()
	lbt := atomic.LoadInt64(&bs[breakName].lastBreakTime)
	// 若熔断期未过，返回预设熔断处理
	if lbt != 0 && lbt+int64(bs[breakName].BreakTime) > now {
		return fallback()
	}

	// 当前请求次数+1（写锁）
	var countLock sync.Mutex
	countLock.Lock()
	bs[breakName].nowCount++
	countLock.Unlock()

	res, err := run()
	// 若超时或返回失败则当前失败次数+1（写锁）
	if err != nil {
		var failCountLock sync.Mutex
		failCountLock.Lock()
		bs[breakName].failCount++
		failCountLock.Unlock()
	}

	// 读取当前请求次数及当前失败次数（读锁），判断是否达到请求阈值及失败率
	var readCountLock sync.RWMutex
	readCountLock.Lock()
	c := bs[breakName].nowCount
	fc := bs[breakName].failCount
	readCountLock.Unlock()
	if c < bs[breakName].BreakCount || fc < c*bs[breakName].FailPercent/100 {
		return res, err
	}

	now = time.Now().Unix()
	// 若达到，读取上次熔断时间（原子操作）
	lbt = atomic.LoadInt64(&bs[breakName].lastBreakTime)
	// 若熔断期未过，返回预设熔断处理
	if lbt != 0 && lbt+int64(bs[breakName].BreakTime) > now {
		return fallback()
	}

	// 若熔断期已过，更新熔断时间（原子操作）
	swapped := atomic.CompareAndSwapInt64(&bs[breakName].lastBreakTime, lbt, now)

	// 并重置请求次数失败次数（写锁）
	if swapped {
		var initCountLock sync.Mutex
		initCountLock.Lock()
		bs[breakName].nowCount = 0
		bs[breakName].failCount = 0
		initCountLock.Unlock()
	}

	if lbt == 0 {
		return fallback()
	}
	return res, err
}

// 使用示例
func main() {
	breakname := "test1"
	bc := Breaker{
		BreakCount:  10,
		FailPercent: 10,
		BreakTime:   5,
	}
	bs[breakname] = &bc

	run := func() (string, error) {
		return "running", nil
	}
	bad := func() (string, error) {
		return "bad", errors.New("error ...")
	}
	fallback := func() (string, error) {
		return "fallback", nil
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func(wg *sync.WaitGroup) {
		for i := 0; i < 20; i++ {
			time.Sleep(time.Second)
			if i == 4 {
				log.Println(BreakHandle(breakname, bad, fallback))
			} else {
				log.Println(BreakHandle(breakname, run, fallback))
			}
		}
		wg.Done()
	}(&wg)
	go func(wg *sync.WaitGroup) {
		for i := 0; i < 20; i++ {
			time.Sleep(time.Second)
			if i == 14 {
				log.Println(BreakHandle(breakname, bad, fallback))
			} else {
				log.Println(BreakHandle(breakname, run, fallback))
			}
		}
		wg.Done()
	}(&wg)
	wg.Wait()

}
